require 'spec_helper'
require 'faker'

RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do
  def random_string(min_len = 1, max_len = 12)
    str = Faker::Alphanumeric.alpha(number: max_len)
    str[0, rand(min_len..max_len)]
  end

  let(:cookie) { described_class.new(random_string, random_string) }

  before(:each) do
    Timecop.freeze(Time.local(2008, 9, 5, 10, 5, 30))
  end

  after(:each) do
    Timecop.return
  end

  describe 'name' do
    describe 'String' do
      it 'is assigned to name successfully' do
        n = random_string

        cookie.name = n

        expect(cookie.name).to eql(n)
        expect(cookie.name.class).to eql(String)
      end

      it 'that is empty is passed to name and throws an ArgumentError' do
        n = ''

        expect do
          cookie.name = n
        end.to raise_error(ArgumentError)
      end
    end

    describe 'nil' do
      it 'assigned to name throws an ArgumentError' do
        n = nil

        expect do
          cookie.name = n
        end.to raise_error(ArgumentError)
      end
    end
  end

  describe 'value' do
    describe 'String' do
      it 'is assigned to value successfully' do
        v = random_string

        cookie.value = v

        expect(cookie.value).to eql(v)
        expect(cookie.value.class).to eql(String)
      end

      it 'that is empty is passed to value successfully' do
        v = ''

        cookie.value = v

        expect(cookie.value).to eql(v)
        expect(cookie.value.class).to eql(String)
      end
    end

    describe 'nil' do
      it 'assigned to value results in it being set to an empty string and expires is set UNIX_EPOCH' do
        v = nil

        cookie.value = v

        expect(cookie.value).to eql('')
        expect(cookie.value.class).to eql(String)
        expect(cookie.expires).to eql(Time.at(0))
      end
    end
  end

  describe 'max_age' do
    describe 'Integer' do
      it 'assigns an integer of 0 to max_age successfully' do
        int = 0

        cookie.max_age = int

        expect(cookie.max_age).to eql(int)
      end

      it 'assigns a positive integer to max_age successfully' do
        pos_val = rand(1..100)

        cookie.max_age = pos_val

        expect(cookie.max_age).to eql(pos_val)
      end

      it 'assigns a negative integer to max_age successfully' do
        neg_val = rand(-1..-100)

        cookie.max_age = neg_val

        expect(cookie.max_age).to eql(neg_val)
      end
    end

    describe 'String' do
      it 'assigns a String of 0 to max_age successfully' do
        str = '0'

        cookie.max_age = str

        expect(cookie.max_age).to eql(str.to_i)
      end

      it 'assigns a String of a positive Integer to max_age successfully' do
        pos_val = rand(1..100)

        cookie.max_age = pos_val.to_s

        expect(cookie.max_age).to eql(pos_val)
      end

      it 'assigns a String of a negative Integer to max_age successfully' do
        neg_val = rand(-100..-1)

        cookie.max_age = neg_val.to_s

        expect(cookie.max_age).to eql(neg_val)
      end

      it 'throws an ArgumentError with a String that cannot be converted into an Integer' do
        invalid_str = random_string

        expect do
          cookie.max_age = invalid_str
        end.to raise_error(ArgumentError)
      end
    end

    describe 'Complex Object' do
      it 'throws an TypeError with a Complex Object that cannot be converted into an Integer' do
        obj = [rand(9), rand(9), rand(9)]

        expect do
          cookie.max_age = obj
        end.to raise_error(TypeError)
      end
    end

    describe 'nil' do
      it 'assigns a nil to max_age successfully' do
        cookie.max_age = nil
        expect(cookie.max_age).to eql(nil)
      end
    end
  end

  describe 'expires' do
    describe 'Time' do
      it 'instance is assigned to expires successfully' do
        t = Time.now

        cookie.expires = t

        expect(cookie.expires).to eql(t)
        expect(cookie.expires.class).to eql(Time)
      end
    end

    describe 'Non-Time' do
      it 'which can be parsed to a Time object, will be assigned to expires successfully' do
        obj = Time.now.to_s

        cookie.expires = obj

        expect(cookie.expires).to eql(Time.parse(obj))
      end

      it 'which cannot be parsed to a Time object, when assigned to expires, will throw a TypeError' do
        obj = [rand(9), rand(9), rand(9)]

        expect do
          cookie.expires = obj
        end.to raise_error(TypeError)
      end
    end

    describe 'nil' do
      it 'assigned to expires successfully' do
        cookie.expires = nil
        expect(cookie.expires).to eql(nil)
      end
    end
  end

  describe 'path' do
    describe 'String' do
      it 'beginning with "/" is passed to "path" successfully' do
        p = '/' + random_string

        cookie.path = p

        expect(cookie.path).to eql(p)
        expect(cookie.path.class).to eql(String)
      end

      it 'not beginning with "/" is passed to "path" and is set as "/"' do
        p = random_string

        cookie.path = p

        expect(cookie.path).to eql('/')
        expect(cookie.path.class).to eql(String)
      end

      it 'that is empty is passed to "path" and is set as "/"' do
        p = ''

        cookie.path = p

        expect(cookie.path).to eql('/')
        expect(cookie.path.class).to eql(String)
      end
    end

    # If the Object A responds to to_str with a truthy Object that responds true to start_with?('/'),
    # then path is set to A
    describe 'Complex Object' do
      it 'which is a kind of String will be passed to path successfully' do
        clazz = Class.new(String) do
        end
        str = "/#{random_string(0)}"
        my_str = clazz.new(str)

        cookie.path = my_str

        expect(cookie.path).to eql(str)
      end
    end

    describe 'nil' do
      it 'assigned to expired successfully' do
        n = nil

        cookie.expires = n

        expect(cookie.expires).to eql(n)
      end
    end
  end

  describe 'domain' do
    describe 'DomainName' do
      it 'assigned to domain will result in a domain based on DomainName.hostname.' do
        # big string length avoids potential TLD values
        d = DomainName(random_string(30,35))

        cookie.domain = d

        expect(cookie.domain).to eql(d.hostname)
      end
    end

    describe 'nil' do
      it 'assigned to domain when origin is not set will result in a nil domain' do
        n = nil

        cookie.domain = n

        expect(cookie.domain).to eql(n)
      end
    end

    describe 'String' do
      it 'assigned to domain will be parsed to a DomainName upon assignment. The hostname of that DomainName will be returned by the getter.' do
        # big string length avoids potential TLD values
        d = DomainName(random_string(30,35))

        cookie.domain = d

        expect(cookie.domain).to eql(DomainName(d).hostname)
      end
    end
  end

  describe 'httponly' do
    describe 'Truthy' do
      it 'empty string passed to httponly is set as true' do
        str = ''

        cookie.httponly = str

        expect(cookie.httponly).to eql(true)
      end

      it 'populated string passed to httponly is set as true' do
        str = random_string

        cookie.httponly = str

        expect(cookie.httponly).to eql(true)
      end

      it 'integer passed to httponly is set as true' do
        int = rand(0..10)

        cookie.httponly = int

        expect(cookie.httponly).to eql(true)
      end

      it 'true passed to httponly is set as true' do
        t = true

        cookie.httponly = t

        expect(cookie.httponly).to eql(true)
      end
    end

    describe 'Falsey' do
      it 'nil passed to httponly is set as false' do
        n = nil

        cookie.httponly = n

        expect(cookie.httponly).to eql(false)
      end

      it 'false passed to httponly is set as false' do
        f = false

        cookie.httponly = f

        expect(cookie.httponly).to eql(false)
      end
    end
  end

  describe 'secure' do
    describe 'Truthy' do
      it 'empty string passed to secure is set as true' do
        str = ''

        cookie.secure = str

        expect(cookie.secure).to eql(true)
      end

      it 'populated string passed to secure is set as true' do
        str = random_string

        cookie.secure = str

        expect(cookie.secure).to eql(true)
      end

      it 'integer passed to secure is set as true' do
        int = rand(0..10)

        cookie.secure = int

        expect(cookie.secure).to eql(true)
      end

      it 'true passed to secure is set as true' do
        t = true

        cookie.secure = t

        expect(cookie.secure).to eql(true)
      end
    end

    describe 'Falsey' do
      it 'nil passed to secure is set as false' do
        n = nil

        cookie.secure = n

        expect(cookie.secure).to eql(false)
      end

      it 'false passed to secure is set as false' do
        f = false

        cookie.secure = f

        expect(cookie.secure).to eql(false)
      end
    end
  end

  describe 'origin' do
    context 'when assigned a String' do
      it 'is assigned to origin successfully' do
        n = random_string

        cookie.origin = n

        expect(cookie.origin).to eql(URI(n))
        expect(cookie.origin.class).to eql(URI::Generic)
      end

      it 'that is empty is passed to name and throws an ArgumentError' do
        n = ''

        expect do
          cookie.name = n
        end.to raise_error(ArgumentError)
      end

      it 'representing a http URI will be passed to origin successfully, and populate the domain and path fields correctly' do
        domain = random_string(5)
        path = "/#{random_string(5)}/"
        protocol = "http"
        my_str = "#{protocol}://#{domain}#{path}"

        cookie.origin = my_str

        expect(cookie.origin).to eql(URI(my_str))
        expect(cookie.domain).to eql(domain)
        expect(cookie.path).to eql(path)
        expect(cookie.origin.class).to eql(URI::HTTP)
      end

      it 'representing a https URI will be passed to origin successfully, and populate the domain and path fields correctly' do
        domain = random_string(5)
        path = "/#{random_string(5)}/"
        protocol = "https"
        my_str = "#{protocol}://#{domain}#{path}"

        cookie.origin = my_str

        expect(cookie.origin).to eql(URI(my_str))
        expect(cookie.domain).to eql(domain)
        expect(cookie.path).to eql(path)
        expect(cookie.origin.class).to eql(URI::HTTPS)
      end
    end

    describe 'immutable' do
      it 'when origin is set previously, assigning a value to it throws an ArgumentError' do
        origin = random_string
        unused_origin = random_string
        cookie.origin = origin

        expect do
          cookie.origin = unused_origin
        end.to raise_error(ArgumentError)
      end
    end

    context 'when assigned a nil value' do
      it 'assigned to name throws an ArgumentError' do
        n = nil

        expect do
          cookie.name = n
        end.to raise_error(ArgumentError)
      end
    end

    # If the Object A responds to to_str with a truthy Object, then origin is set to A
    context 'when assigned a Complex Objext' do
      it 'which responds to to_str will be passed to origin successfully' do
        clazz = Class.new(String) do
        end
        str = "#{random_string(5)}"
        my_str = clazz.new(str)

        cookie.origin = my_str

        expect(cookie.origin).to eql(URI(my_str))
        expect(cookie.domain).to eql(nil)
        expect(cookie.path).to eql(nil)
      end

      it 'which does not respond to to_str will cause an ArgumentError to be thrown' do
        clazz = Class.new do
        end
        my_str = clazz.new

        expect do
          cookie.origin = my_str
        end.to raise_error(ArgumentError)
      end

      it 'which is a kind of String representing a URI will be passed to origin successfully, and populate the domain and path fields correctly' do
        clazz = Class.new(String) do
        end
        domain = random_string(5)
        path = "/#{random_string(5)}/"
        protocol = "http#{rand(2) == 1 ? 's' : ''}"

        str = "#{protocol}://#{domain}#{path}"
        my_str = clazz.new(str)

        cookie.origin = my_str

        expect(cookie.origin).to eql(URI(my_str))
        expect(cookie.domain).to eql(domain)
        expect(cookie.path).to eql(path)
        expect(cookie.origin.class).to eql(protocol.end_with?('s') ? URI::HTTPS : URI::HTTP)
      end
    end
  end

  describe 'created_at' do
    describe 'Time' do
      it 'is returned by created_at after the cookie is initialized' do
        # cookie created in before

        c = cookie.created_at

        expect(c.class).to eql(Time)
      end

      it 'instance is assigned to created_at successfully' do
        t = Time.now

        cookie.created_at = t

        expect(cookie.created_at).to eql(t)
        expect(cookie.created_at.class).to eql(Time)
      end
    end

    describe 'Non-Time' do
      it 'which can be parsed to a Time object, will be assigned to created_at successfully' do
        obj = Time.now.to_s

        cookie.created_at = obj

        expect(cookie.created_at).to eql(Time.parse(obj))
      end

      it 'which cannot be parsed to a Time object, when assigned to created_at, will throw a TypeError' do
        obj = [rand(9), rand(9), rand(9)]

        expect do
          cookie.created_at = obj
        end.to raise_error(TypeError)
      end
    end

    describe 'nil' do
      it 'assigned to created_at successfully' do
        cookie.created_at = nil
        expect(cookie.created_at).to eql(nil)
      end
    end
  end

  describe 'accessed_at' do
    describe 'Time' do
      it 'is returned by accessed_at after the cookie is initialized' do
        # cookie created in before

        c = cookie.accessed_at

        expect(c.class).to eql(Time)
      end

      it 'instance is assigned to accessed_at successfully' do
        t = Time.now

        cookie.accessed_at = t

        expect(cookie.accessed_at).to eql(t)
        expect(cookie.accessed_at.class).to eql(Time)
      end
    end

    describe 'Non-Time' do
      it 'which can be parsed to a Time object, will be assigned to accessed_at successfully' do
        obj = Time.now.to_s

        cookie.accessed_at = obj

        expect(cookie.accessed_at).to eql(Time.parse(obj))
      end

      it 'which cannot be parsed to a Time object, when assigned to accessed_at, will throw a TypeError' do
        obj = [rand(9), rand(9), rand(9)]

        expect do
          cookie.accessed_at = obj
        end.to raise_error(TypeError)
      end
    end

    describe 'nil' do
      it 'assigned to accessed_at successfully' do
        cookie.accessed_at = nil
        expect(cookie.accessed_at).to eql(nil)
      end
    end
  end

  describe 'session' do
    it 'is set as True when initialized with no max_age or expires args and a nil value' do
      # cookie created in before

      s = cookie.session?

      expect(s).to eql(true)
    end

    it 'is set to true when nil is assigned to max_age' do
      max_age = nil

      cookie.max_age = max_age

      expect(cookie.session?).to eq(true)
    end

    it 'is set to false when an integer > 0 is assigned to max_age' do
      max_age = Faker::Number.within(range: 1..100)

      cookie.max_age = max_age

      expect(cookie.session?).to eq(false)
    end

    it 'is set to true when nil is assigned to expires' do
      expires = nil

      cookie.expires = expires

      expect(cookie.session?).to eq(true)
    end

    it 'is set to false when a valid Time is assigned to expires' do
      expires = Time.now

      cookie.expires = expires

      expect(cookie.session?).to eq(false)
    end
  end

  describe 'expired?' do
    describe 'nil' do
      it 'assigned to expires causes expired? to return false' do
        n = nil

        cookie.expires = n

        expect(cookie.expired?).to eql(false)
      end
    end

    describe 'No-Arg' do
      it 'call of expired? returns false when expires and max_age are set to nil' do
        n = nil

        cookie.expires = n

        expect(cookie.expired?).to eql(false)
      end

      it 'call of expired? will return true when expires is set to a Time in the past' do
        t = Faker::Time.backward(days: 1)

        cookie.expires = t

        expect(cookie.expired?).to eql(true)
      end

      it 'call of expired? will return false when expires is set to a Time in the future' do
        t = Faker::Time.forward(days: 1)

        cookie.expires = t

        expect(cookie.expired?).to eql(false)
      end

      it 'call of expired? will return true when max_age in seconds plus cookie.created_at is before Time.now' do
        max_age = 1

        cookie.max_age = max_age
        cookie.created_at = Time.local(2008, 9, 5, 10, 5, 30) - 5.seconds

        expect(cookie.expired?).to eql(true)
      end

      it 'call of expired? will return false when max_age in seconds plus cookie.created_at is after Time.now' do
        max_age = 10

        cookie.max_age = max_age
        cookie.created_at = Time.local(2008, 9, 5, 10, 5, 30) - 5.seconds

        expect(cookie.expired?).to eql(false)
      end
    end

    describe 'Time' do
      it 'call of expired? with any Time value returns false when expires is set to nil' do
        n = nil

        cookie.expires = n
        cookie.max_age = n

        expect(cookie.expired?(Time.now)).to eql(false)
      end

      it 'before "cookie.expires" is passed to expired? will return false' do
        t = Faker::Time.backward(days: 1)

        cookie.expires = Time.now

        expect(cookie.expired?(t)).to eq(false)
      end

      it 'after "cookie.expires" is passed to expired? will return true' do
        t = Faker::Time.forward(days: 1)

        cookie.expires = Time.now

        expect(cookie.expired?(t)).to eql(true)
      end

      it 'passed to expired?, will return true when cookie.created_at + max_age.seconds is before the passed value' do
        t = Faker::Time.forward(days: 1)

        cookie.max_age = 1
        cookie.created_at = Time.now

        expect(cookie.expired?(t)).to eq(true)
      end

      it 'passed to expired?, will return false when created_at + seconds.max_age is after the passed value' do
        t = Faker::Time.backward(days: 1)

        cookie.max_age = 1
        cookie.created_at = Time.now

        expect(cookie.expired?(t)).to eq(false)
      end
    end
  end

  describe 'valid_for_uri?' do
    it "will return false when domain hasn't been set" do
      # domain set as nil in before

      expect do
        cookie.valid_for_uri?(URI(random_string))
      end.to raise_error(ArgumentError)
    end

    it 'will return false if secure is set as true and the passed cookie is a https url' do
      cookie.domain = random_string

      v = cookie.valid_for_uri?(URI("https://#{random_string}"))

      expect(v).to eq(false)
    end
  end

  describe 'acceptable_from_uri?' do
    it 'will return false when passed nil' do
      n = nil

      a = cookie.acceptable_from_uri?(n)

      expect(a).to eq(false)
    end

    it 'will return false if url without http or https protocol is passed' do
      generic_uri = random_string

      v = cookie.acceptable_from_uri?(generic_uri)

      expect(v).to eq(false)
    end

    it 'will return false if url with http protocol is passed but has no host' do
      protocol = 'http://'

      v = cookie.acceptable_from_uri?(protocol)

      expect(URI(protocol).is_a?(::URI::HTTP)).to eq(true)
      expect(v).to eq(false)
    end

    it 'will return false if url with https protocol is passed but has no host' do
      protocol = 'https://'

      v = cookie.acceptable_from_uri?(protocol)

      expect(URI(protocol).is_a?(::URI::HTTP)).to eq(true)
      expect(v).to eq(false)
    end

    it 'will return true if url with http protocol is passed with a domain that matches the url domain' do
      host = random_string
      uri = "http://#{host}/#{random_string}"
      cookie.domain = host

      v = cookie.acceptable_from_uri?(uri)

      expect(v).to eq(true)
    end

    it 'will return true if url with https protocol is passed with a domain that matches the url domain' do
      host = random_string
      uri = "https://#{host}/#{random_string}"
      cookie.domain = host

      v = cookie.acceptable_from_uri?(uri)

      expect(v).to eq(true)
    end

    it "will return true if url with http protocol is passed to a nil domain" do
      uri = "http://#{random_string}/#{random_string}"
      cookie.domain = nil

      v = cookie.acceptable_from_uri?(uri)

      expect(v).to eq(cookie.domain.nil?)
    end

    it "will return false if url with http protocol is passed with a domain that doesn't match the cookie domain" do
      uri = "http://#{random_string}/#{random_string}"
      cookie.domain = random_string + 'cookie_domain'

      v = cookie.acceptable_from_uri?(uri)

      expect(v).to eq(cookie.domain.nil?)
    end


    it "will return true if url with https protocol is passed to a nil domain" do
      uri = "https://#{random_string}/#{random_string}"
      cookie.domain = nil

      v = cookie.acceptable_from_uri?(uri)

      expect(v).to eq(cookie.domain.nil?)
    end

    it "will return false if url with https protocol is passed with a domain that doesn't match the cookie domain" do
      uri = "https://#{random_string}/#{random_string}"
      cookie.domain = random_string + 'cookie_domain'

      v = cookie.acceptable_from_uri?(uri)

      expect(v).to eq(cookie.domain.nil?)
    end
  end
end
